{*******************************************************************************
* The contents of this file are used with permission, subject to the Mozilla   *
* Public License Version 1.1 (the "License"); you may not use this file except *
* in compliance with the License. You may obtain a copy of the License at      *
* http://www.mozilla.org/MPL/                                                  *
*                                                                              *
* Software distributed under the License is distributed on an "AS IS" basis,   *
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for *
* the specific language governing rights and limitations under the License.    *
*                                                                              *
* The Original Code is distributed as part of the "mediate" product and is     *
* Copyright (C) @home media limited. All Rights Reserved.                      *
*                                                                              *
* Email: support@athomemedia.co.uk                                             *
* Web:   http://www.athomemedia.co.uk                                          *
*******************************************************************************}

unit BaseControls;

interface

uses Classes, AHMTypes, UIObjects, TagNames, Viewport, Places, Layouts, Actions,
     Colour, Bitmaps, Effects, RenderCache, Scrolling, XMLHelper;

const
  ControlStateNames: array[0..7] of String =
    (STATE_VAL_IDLE, STATE_VAL_FOCUS, STATE_VAL_PRESS, STATE_VAL_DISABLE,
     STATE_VAL_IDLE_CHECK, STATE_VAL_FOCUS_CHECK, STATE_VAL_PRESS_CHECK,
     STATE_VAL_DISABLE_CHECK);
  DefaultIdleTimeout = 5000; // 5 seconds idle timeout
  FocusFadeDuration = 500; // Duration in ms for focus bitmap fading
  PressFadeDuration = 750; // Duration in ms for pressed bitmap fading

  DEFAULT_CTRL_WIDTH = 300; // Default width for a new control or style
  DEFAULT_CTRL_HEIGHT = 100; // Default height for a new control or style
  DEFAULT_CTRL_TEXT = 'Lorem ipsum dolor'; // Default text for new control

{$IFDEF DESIGNER}
  MODIFY_TEXT = 'Edit Text';
  MODIFY_ACTIONS = 'Edit Actions';
  MODIFY_HINT = 'Edit Hint';
  CAPTION_TEXT = 'Text';
  CAPTION_HINT = 'Hint';

  // Property categories and hints for designer
  CAT_DESIGN = 'Design';
  CAT_APPEARANCE = 'Appearance';
  CAT_CONTENT = 'Content';
  CAT_NAVIGATION = 'Navigation';
  HINT_WIDTH = 'specifies width for the control (applied to associated style)';
  HINT_HEIGHT = 'specifies height for the control (applied to associated style)';
  HINT_PLACEMENT = 'defines position and orientation for this control (unless using a layout)';
  HINT_DEPTH = 'allows drawing order to be overridden if required (leave as mid ground for default)';
  HINT_LAYOUT = 'defines a layout to use for this control (overrides placement)';
  HINT_LAYOUTROW = 'sets a specific grid layout row to use (leave as 0 for default)';
  HINT_LAYOUTCOL = 'sets a specific grid layout column to use (leave as 0 for default)';
  HINT_LAYOUTPOS = 'sets a specific custom layout position to use (leave as 0 for default)';
  HINT_VISIBLE = 'controls whether or not the control is displayed';
  HINT_STYLE = 'specifies the visual style for drawing the control';
  HINT_TEXT = 'specifies the name of the text string to show in the control';
  HINT_METAVALUE = 'specifies the name of the meta data value to show in the control (overrides text)';
  HINT_METAGROUP = 'specifies the name of the meta data group to use in combination with meta value';
  HINT_METAINDEX = 'specifies the name of the meta data index to use in combination with meta value';
  HINT_ACTIONS = 'defines action handling for this control';
  HINT_HINT = 'specifies the name of the hint text string for the control';
  HINT_ENABLED = 'determines whether the control responds to keyboard events';
  HINT_MOUSEENABLED = 'determines whether the control responds to mouse events';
  HINT_EFFECT = 'defines visual effects for this control';
  HINT_FOCUSEFFECT = 'defines visual effects for this control when it has focus';
  HINT_PRESSEFFECT = 'defines visual effects for this control when it is pressed';
  HINT_CHECKEFFECT = 'defines visual effects for this control when it is checked';
  HINT_UPCONTROL = 'overrides the normal navigation behaviour when moving up';
  HINT_DOWNCONTROL = 'overrides the normal navigation behaviour when moving down';
  HINT_LEFTCONTROL = 'overrides the normal navigation behaviour when moving left';
  HINT_RIGHTCONTROL = 'overrides the normal navigation behaviour when moving right';
{$ENDIF}

type
  TAHMAbstractControl = class(TAHMUIObject)
  private
    FVirtWidth: Integer;
    FVirtHeight: Integer;
    FAnimating: Boolean;
    FVisible: Boolean;
    FInitiallyVisible: Boolean;
    FPlacement: TAHMPlace;
    FSized: Boolean;
    FLoaded: Boolean;
    FInitialisedEffect: Boolean;
    FEffect: TAHMEffect;
    FGamma: TAHMCanvasColour;
    FOwnGamma: TAHMCanvasColour;
    FOwnXScale: Single;
    FOwnYScale: Single;
  protected
    FRenderRect: TAHMRectF;
    FRenderCache: TAHMRenderCache;
    FRenderState: TAHMControlState;
    FViewport: TAHMViewport;
    FWidth, FHeight: Single;
    FInitWidth, FInitHeight: Single;
    FXPos, FYPos: Single;
    FInitXPos, FInitYPos: Single;
    FXRot, FYRot, FZRot: Single;
    FXFlip, FYFlip: Boolean;
    FValid: Boolean;
    FCentreX, FCentreY: Single;
    FNavigationX, FNavigationY: Single;
    FDepth: TAHMDepthType;
    function GetGamma: TAHMCanvasColour; virtual; abstract;
    function GetOwnScale: Single;
    procedure SetOwnScale(Scale: Single);
    function GetXScale: Single; virtual; abstract;
    function GetYScale: Single; virtual; abstract;
    function GetVirtualWidth: Integer; virtual;
    function GetVirtualHeight: Integer; virtual;
    procedure SetVirtualWidth(Width: Integer); virtual;
    procedure SetVirtualHeight(Height: Integer); virtual;
    function GetDesignerWidth: Integer; virtual; abstract;
    function GetDesignerHeight: Integer; virtual; abstract;
    function GetDesignerPos: String; override;
    procedure SetDesignerPos(Position: String); override;
    function GetDesignerRot: String; override;
    procedure SetDesignerRot(Rotation: String); override;
    function GetVirtualXPos: Integer;
    procedure SetVirtualXPos(XPos: Integer);
    function GetVirtualYPos: Integer;
    procedure SetVirtualYPos(YPos: Integer);
    function GetState: TAHMControlState; virtual;
    procedure SetPlacement(Place: TAHMPlace);
    procedure SetEffect(Effect: TAHMEffect);
    procedure SetInitiallyVisible(InitiallyVisible: Boolean);
    procedure InvalidateEffect;
    procedure InitialiseEffect; virtual;
    procedure Loaded; virtual;
    procedure ClearProperties; override;
    property ControlLoaded: Boolean read FLoaded;
  public
    constructor Create(const Name: String); override;
    destructor Destroy; override;
    property Animating: Boolean read FAnimating write FAnimating;
    property Visible: Boolean read FVisible write FVisible;
    property OwnScale: Single read GetOwnScale write SetOwnScale;
    property OwnXScale: Single read FOwnXScale write FOwnXScale;
    property OwnYScale: Single read FOwnYScale write FOwnYScale;
    property Gamma: TAHMCanvasColour read GetGamma;
    property OwnGamma: TAHMCanvasColour read FOwnGamma;
    property XScale: Single read GetXScale;
    property YScale: Single read GetYScale;
    property XPos: Single read FXPos;
    property YPos: Single read FYPos;
    property VirtualXPos: Integer read GetVirtualXPos write SetVirtualXPos;
    property VirtualYPos: Integer read GetVirtualYPos write SetVirtualYPos;
    property InitialXPos: Single read FInitXPos;
    property InitialYPos: Single read FInitYPos;
    property XRotation: Single read FXRot;
    property YRotation: Single read FYRot;
    property ZRotation: Single read FZRot;
    property VerticalFlip: Boolean read FYFlip write FYFlip;
    property HorizontalFlip: Boolean read FXFlip write FXFlip;
    property DesignerWidth: Integer read GetDesignerWidth;
    property DesignerHeight: Integer read GetDesignerHeight;
    property Width: Single read FWidth write FWidth;
    property Height: Single read FHeight write FHeight;
    property InitialWidth: Single read FInitWidth;
    property InitialHeight: Single read FInitHeight;
    property Viewport: TAHMViewport read FViewport write FViewport;
    property ControlState: TAHMControlState read GetState;
    property Valid: Boolean read FValid write FValid;
    property Sized: Boolean read FSized;
    property InitialisedEffect: Boolean read FInitialisedEffect;
    property RenderRect: TAHMRectF read FRenderRect write FRenderRect;
    property RenderCache: TAHMRenderCache read FRenderCache;
    function DesignerAdjustPos(DX, DY: Integer): Boolean; override;
    function DesignerAdjustRot(DX, DY, DZ: Single): Boolean; override;
    function CanvasRect(X, Y, Width, Height: Single): TAHMRectF; virtual;
    function ClipRect: TAHMRectF; virtual;
    procedure MoveControl(XPos, YPos: Single); virtual;
    procedure SizeControl(Width, Height: Single); virtual;
    procedure RotateControl(XRot, YRot, ZRot: Single); virtual;
    function BoundsPoint(XPos, YPos: Single): Boolean; virtual;
    function PageScroll(Direction: TAHMDirection): Boolean; virtual;
    function Scroll(Direction: TAHMDirection): Boolean; virtual;
    function SmoothScroll(DX, DY: Single): Boolean; virtual;
    function RenderPreview: Boolean; override;
    procedure Invalidate; virtual;
    procedure InvalidateSize; virtual;
    procedure Rendering; virtual;
    procedure Rendered; virtual;
    procedure Render(Viewport: TAHMViewport); virtual;
    procedure Resize; virtual; abstract;
    procedure Resized; virtual;
    procedure Assign(Source: TPersistent); override;
    procedure ParseCommonXML(XML: TAHMXMLHelper); override;
    procedure BuildProperties(Properties: TAHMUIPropertyList); override;
  published
    property VirtualWidth: Integer read GetVirtualWidth write SetVirtualWidth;
    property VirtualHeight: Integer read GetVirtualHeight write SetVirtualHeight;
    property Effect: TAHMEffect read FEffect write SetEffect;
    property Depth: TAHMDepthType read FDepth write FDepth;
    property Placement: TAHMPlace read FPlacement write SetPlacement;
    property InitiallyVisible: Boolean read FInitiallyVisible write SetInitiallyVisible;
  end;

  TAHMControl = class(TAHMAbstractControl)
  private
    FText: TAHMTextString;
    FDisplayText: String;
    FMetaValue: TAHMMetaString;
    FMetaGroup: TAHMMetaString;
    FMetaIndex: TAHMMetaString;
    FScroller: TAHMAbstractScroller;
    FLayoutLocn: TAHMLayoutLocation;
    FDrawBorder: Boolean;
    FStyleName: String;
    FStyle: TAHMUIDisplayObject;
    FDefaultStyle: TAHMUIDisplayObject;
  protected
    function TagBegin: String; override;
    function GetGamma: TAHMCanvasColour; override;
    function GetXScale: Single; override;
    function GetYScale: Single; override;
    function GetVirtualWidth: Integer; override;
    function GetVirtualHeight: Integer; override;
    procedure SetVirtualWidth(Width: Integer); override;
    procedure SetVirtualHeight(Height: Integer); override;
    function GetDesignerWidth: Integer; override;
    function GetDesignerHeight: Integer; override;
    function GetDesignerPos: String; override;
    procedure SetDesignerPos(Position: String); override;
    function GetDesignerSize: String; override;
    procedure SetDesignerSize(Size: String); override;
    function GetLayout: TAHMLayout;
    procedure SetLayout(Layout: TAHMLayout);
    function GetLayoutRow: Integer;
    procedure SetLayoutRow(Row: Integer);
    function GetLayoutCol: Integer;
    procedure SetLayoutCol(Col: Integer);
    function GetLayoutPos: Integer;
    procedure SetLayoutPos(Pos: Integer);
    function GetDisplayMeta: String; virtual;
    function GetDisplayText: String; virtual;
    procedure SetText(NewText: TAHMTextString); virtual;
    procedure SetStyleName(Name: String); virtual;
    procedure SetStyle(Style: TAHMUIDisplayObject);
    function GetDefaultStyle: TAHMUIDisplayObject;
    function GetStyles: TAHMUIObjectList; virtual;
    function GetOverlayRect: TAHMRectF; virtual;
    procedure InvalidateStyle; virtual;
    procedure ClearProperties; override;
    procedure RenderEffects; virtual;
  public
    constructor Create(const Name: String); override;
    destructor Destroy; override;
    property DisplayText: String read GetDisplayText write FDisplayText;
    property Scroller: TAHMAbstractScroller read FScroller write FScroller;
    property DrawBorder: Boolean read FDrawBorder write FDrawBorder;
    property DefaultStyle: TAHMUIDisplayObject read GetDefaultStyle;
    property Styles: TAHMUIObjectList read GetStyles;
    property StyleName: String read FStyleName write SetStyleName;
    property CentreX: Single read FNavigationX;
    property CentreY: Single read FNavigationY;
    function DesignerAdjustPos(DX, DY: Integer): Boolean; override;
    function DesignerAdjustRot(DX, DY, DZ: Single): Boolean; override;
    function DesignerAdjustSize(DX, DY: Integer): Boolean; override;
    function ModifyActions: Integer; override;
    function Modify(Action: Integer): Boolean; override;
    function ModifyCaption(Action: Integer): String; override;
    procedure InvalidateUIObject(UIObject: TAHMUIObject); override;
    procedure Render(Viewport: TAHMViewport); override;
    procedure Resize; override;
    procedure SeedObject; override;
    procedure Assign(Source: TPersistent); override;
    procedure SaveAsXML(Lines: TStrings); override;
    procedure ParseFromXML(XML: TAHMXMLHelper); override;
    procedure BuildProperties(Properties: TAHMUIPropertyList); override;
    class function GetStyleList: TAHMUIObjectList; virtual;
  published
    property Style: TAHMUIDisplayObject read FStyle write SetStyle;
    property Layout: TAHMLayout read GetLayout write SetLayout;
    property LayoutRow: Integer read GetLayoutRow write SetLayoutRow;
    property LayoutColumn: Integer read GetLayoutCol write SetLayoutCol;
    property LayoutPosition: Integer read GetLayoutPos write SetLayoutPos;
    property Location: TAHMLayoutLocation read FLayoutLocn write FLayoutLocn;
    property Text: TAHMTextString read FText write SetText;
    property MetaValue: TAHMMetaString read FMetaValue write FMetaValue;
    property MetaGroup: TAHMMetaString read FMetaGroup write FMetaGroup;
    property MetaIndex: TAHMMetaString read FMetaIndex write FMetaIndex;
  end;

  TAHMControlClass = class of TAHMControl;

  TAHMInteractiveControl = class(TAHMControl)
  private
    FEnabled: Boolean;
    FInitiallyEnabled: Boolean;
    FMouseEnabled: Boolean;
    FInitiallyMouseEnabled: Boolean;
    FKeyEnabled: Boolean;
    FFocused: Boolean;
    FPressed: Boolean;
    FChecked: Boolean;
    FFocusedEffect: TAHMEffect;
    FPressedEffect: TAHMEffect;
    FCheckedEffect: TAHMEffect;
    FFocusedAnimating: Boolean;
    FPressedAnimating: Boolean;
    FFocusedStartTime: TAHMTimeStamp;
    FPressedStartTime: TAHMTimeStamp;
    FHint: TAHMTextString;
    FActions: TAHMActionList;
    FControlUp, FControlDown: String;
    FControlLeft, FControlRight: String;
    FMouseX, FMouseY: Single;
    FMouseDX, FMouseDY: Single;
    FMouseDownX, FMouseDownY: Single;
    FLastBusyTime: TAHMTimestamp;
    FIdleTimeout: TAHMTimestamp;
    FControlIdled: Boolean;
  protected
    procedure SetInitiallyEnabled(InitiallyEnabled: Boolean);
    procedure SetInitiallyMouseEnabled(InitiallyMouseEnabled: Boolean);
    function GetChecked: Boolean; virtual;
    procedure SetChecked(Checked: Boolean); virtual;
    procedure SetFocused(Focused: Boolean);
    procedure SetFocusedEffect(Effect: TAHMEffect);
    procedure SetPressedEffect(Effect: TAHMEffect);
    procedure SetCheckedEffect(Effect: TAHMEffect);
    function GetNavigateUp: TAHMInteractiveControl;
    procedure SetNavigateUp(Control: TAHMInteractiveControl);
    function GetNavigateDown: TAHMInteractiveControl;
    procedure SetNavigateDown(Control: TAHMInteractiveControl);
    function GetNavigateLeft: TAHMInteractiveControl;
    procedure SetNavigateLeft(Control: TAHMInteractiveControl);
    function GetNavigateRight: TAHMInteractiveControl;
    procedure SetNavigateRight(Control: TAHMInteractiveControl);
    function GetState: TAHMControlState; override;
    function GetHintText: String;
    procedure DidSomething;
    procedure NormaliseMouseCoordinates(const X, Y: Single);
    procedure NormaliseMouseScale(var X, Y: Single);
    procedure ClearProperties; override;
    procedure RenderEffects; override;
    procedure InitialiseEffect; override;
  public
    constructor Create(const Name: String); override;
    destructor Destroy; override;
    property Enabled: Boolean read FEnabled write FEnabled;
    property MouseEnabled: Boolean read FMouseEnabled write FMouseEnabled;
    property KeyEnabled: Boolean read FKeyEnabled write FKeyEnabled;
    property IdleTimeout: TAHMTimestamp read FIdleTimeout write FIdleTimeout;
    property Focused: Boolean read FFocused write SetFocused;
    property Pressed: Boolean read FPressed write FPressed;
    property Checked: Boolean read GetChecked write SetChecked;
    property HintText: String read GetHintText;
    property MouseX: Single read FMouseX;
    property MouseY: Single read FMouseY;
    property MouseDeltaX: Single read FMouseDX;
    property MouseDeltaY: Single read FMouseDY;
    property MouseDownX: Single read FMouseDownX;
    property MouseDownY: Single read FMouseDownY;
    property NavigateUpName: String read FControlUp write FControlUp;
    property NavigateDownName: String read FControlDown write FControlDown;
    property NavigateLeftName: String read FControlLeft write FControlLeft;
    property NavigateRightName: String read FControlRight write FControlRight;
    function PreviewMouseDown(X, Y: Single): Boolean; override;
    function PreviewMouseMove(X, Y: Single; Down: Boolean): Boolean; override;
    function PreviewMouseUp(X, Y: Single): Boolean; override;
    function PreviewMouseWheel(Delta: Integer): Boolean; override;
    function ModifyActions: Integer; override;
    function Modify(Action: Integer): Boolean; override;
    function ModifyCaption(Action: Integer): String; override;
    function PageScroll(Direction: TAHMDirection): Boolean; override;
    function Scroll(Direction: TAHMDirection): Boolean; override;
    function SmoothScroll(DX, DY: Single): Boolean; override;
    procedure InvalidateUIObject(UIObject: TAHMUIObject); override;
    procedure MoveControl(XPos, YPos: Single); override;
    procedure SizeControl(Width, Height: Single); override;
    procedure SendKey(KeyCode: String); overload; virtual;
    procedure SendKey(KeyCode: Word; Shift: TShiftState); overload; virtual;
    procedure Press; virtual;
    procedure UnPress; virtual;
    procedure Activate; virtual;
    procedure Cancel; virtual;
    procedure Idle; virtual;
    procedure MouseDown(X, Y: Single);
    procedure MouseUp(X, Y: Single);
    procedure MouseMove(X, Y: Single; Down: Boolean);
    procedure MouseClicked; virtual;
    procedure MouseMoved(Down: Boolean); virtual;
    procedure MouseWheel(Delta: Integer); virtual;
    procedure InitialiseCustomEffects(Focused, Pressed, Checked: Boolean);
    procedure RenderCustomEffects(Overlay: TAHMRectF; DrawFocus, DrawPress, DrawCheck: Boolean);
    procedure Rendering; override;
    procedure Assign(Source: TPersistent); override;
    procedure AssignContents(Source: TAHMUIObject); override;
    procedure SaveAsXML(Lines: TStrings); override;
    procedure BuildProperties(Properties: TAHMUIPropertyList); override;
  published
    property Actions: TAHMActionList read FActions;
    property Hint: TAHMTextString read FHint write FHint;
    property InitiallyEnabled: Boolean read FInitiallyEnabled write SetInitiallyEnabled;
    property InitiallyMouseEnabled: Boolean read FInitiallyMouseEnabled write SetInitiallyMouseEnabled;
    property FocusedEffect: TAHMEffect read FFocusedEffect write SetFocusedEffect;
    property PressedEffect: TAHMEffect read FPressedEffect write SetPressedEffect;
    property CheckedEffect: TAHMEffect read FCheckedEffect write SetCheckedEffect;
    property NavigateUpControl: TAHMInteractiveControl read GetNavigateUp write SetNavigateUp;
    property NavigateDownControl: TAHMInteractiveControl read GetNavigateDown write SetNavigateDown;
    property NavigateLeftControl: TAHMInteractiveControl read GetNavigateLeft write SetNavigateLeft;
    property NavigateRightControl: TAHMInteractiveControl read GetNavigateRight write SetNavigateRight;
  end;

  // ImmutableControl's may inherit from indirect descendants
  TAHMImmutableControl = class(TAHMInteractiveControl);

  TAHMInteractiveControlClass = class of TAHMInteractiveControl;

  TAHMAbstractControlList = class(TAHMUIObjectList)
  public
    constructor Create; override;
    function GetAbstractControl(const Name: String): TAHMAbstractControl;
    function Modify: Boolean; override;
    class function ClassName: ShortString; override;
  end;

  TAHMControlList = class(TAHMUIObjectList)
  private
  protected
    function ClassFromTag(TagName: String): TAHMUIObjectClass; override;
  public
    constructor Create; override;
    function AddControl(const InheritFrom, NewName: String; NewClass: TAHMControlClass): TAHMControl;
    function GetControl(const Name: String): TAHMControl;
    function GetInteractiveControl(const Name: String): TAHMInteractiveControl;
    function Modify: Boolean; override;
  end;


implementation

uses SysUtils, Logging, UIControls, Containers, Languages, ApplicationManager,
     {$IFDEF DESIGNER}DesignerList,{$ENDIF} Math, Canvas, Screen, Timing,
     Utilities, XMLTags, ErrorCodes;

// TAHMAbstractControlList

constructor TAHMAbstractControlList.Create;
begin
  inherited Create;

  XMLTagName := TAG_CONTENTS;
  ChildClass := TAHMAbstractControl;
end;

function TAHMAbstractControlList.GetAbstractControl(const Name: String): TAHMAbstractControl;
begin
  Result := TAHMAbstractControl(GetObject(Name));
end;

function TAHMAbstractControlList.Modify: Boolean;
begin
{$IFDEF DESIGNER}
  // Edit controls and containers with list dialog
  with ApplicationManagerInstance.ThemeManager.InterfaceDescriptor do
    Result := TfrmObjectList.ShowList(Self, ControlList, ContainerList);
{$ELSE}
  Result := False;
{$ENDIF}
end;

class function TAHMAbstractControlList.ClassName: ShortString;
begin
  // Supply more suitable name
  Result := 'Content';
end;

// TAHMControlList

constructor TAHMControlList.Create;
begin
  inherited Create;

  XMLTagName := TAG_CONTROLS;
  ChildClass := TAHMControl;

  // Create empty UI object as an interactive control - we can typecast this
  // as an ancestor type where required. Object will be free'd by default
  // destructor of UIObjectList
  FEmptyObject := TAHMInteractiveControl.Create('');
end;

function TAHMControlList.ClassFromTag(TagName: String): TAHMUIObjectClass;
begin
  Result := nil; // Avoid compiler warning

  // Determine appropriate control to create based on tag
  if      TagName = TAG_CAPTION          then Result := TAHMCaptionControl
  else if TagName = TAG_IMAGE            then Result := TAHMImageControl
  else if TagName = TAG_SHAPE            then Result := TAHMShapeControl
  else if TagName = TAG_CLONE            then Result := TAHMCloneControl
  else if TagName = TAG_BAR              then Result := TAHMBarControl
  else if TagName = TAG_MEDIACONTROL     then Result := TAHMMediaControl
  else if TagName = TAG_GRIDCONTROL      then Result := TAHMGridControl
  else if TagName = TAG_MEDIAGRIDCONTROL then Result := TAHMMediaGridControl
  else if TagName = TAG_BUTTON           then Result := TAHMButtonControl
  else if TagName = TAG_COMBOBUTTON      then Result := TAHMComboButtonControl
  else if TagName = TAG_CHECKBUTTON      then Result := TAHMCheckButtonControl
  else if TagName = TAG_RADIOBUTTON      then Result := TAHMRadioButtonControl
  else if TagName = TAG_TEXTEDIT         then Result := TAHMTextEditControl
  else if TagName = TAG_PLUGINCONTROL    then Result := TAHMPluginControl
  else if TagName = TAG_GAUGECONTROL     then Result := TAHMGaugeControl
  else if TagName = TAG_DIALCONTROL      then Result := TAHMDialControl
  else
    LogParsingError(THM_PRS_INT_UNSUPPORTED_CONTROL_TYPE + TagName, Self);
end;

function TAHMControlList.AddControl(const InheritFrom, NewName: String; NewClass: TAHMControlClass): TAHMControl;
begin
  Result := TAHMControl(Inherit(InheritFrom, NewName, NewClass));
end;

function TAHMControlList.GetControl(const Name: String): TAHMControl;
begin
  Result := TAHMControl(GetObject(Name));
end;

function TAHMControlList.GetInteractiveControl(const Name: String): TAHMInteractiveControl;
var
  Control: TAHMControl;
begin
  // Check type before performing casting as an interactive control
  Control := GetControl(Name);
  if Control is TAHMInteractiveControl then Result := TAHMInteractiveControl(Control)
                                       else Result := nil;
end;

function TAHMControlList.Modify: Boolean;
begin
{$IFDEF DESIGNER}
  // Edit controls with list dialog
  with ApplicationManagerInstance.ThemeManager.InterfaceDescriptor do
    Result := TfrmObjectList.ShowList(Self, ControlList, nil);
{$ELSE}
  Result := False;
{$ENDIF}
end;

//----------------------------------------------------------------------------//

constructor TAHMAbstractControl.Create(const Name: String);
begin
  inherited Create(Name);

  // Create RenderCache & Gamma objects
  FRenderCache := TAHMRenderCache.Create;
  FOwnGamma := TAHMCanvasColour.Create;
  FGamma := TAHMCanvasColour.Create;

  // Set default control properties
  FDepth := dtMidground;
  FOwnXScale := 1.0;
  FOwnYScale := 1.0;
  FVisible := True;
  FInitiallyVisible := True;
end;

destructor TAHMAbstractControl.Destroy;
begin
  // Free RenderCache & Gamma objects
  FRenderCache.Free;
  FOwnGamma.Free;
  FGamma.Free;

  inherited Destroy;
end;

function TAHMAbstractControl.GetOwnScale: Single;
begin
  // Return average of X/Y scales
  Result := (FOwnXScale + FOwnYScale) / 2;
end;

procedure TAHMAbstractControl.SetOwnScale(Scale: Single);
begin
  // Set both X & Y scales to same scale
  FOwnXScale := Scale;
  FOwnYScale := Scale;
end;

function TAHMAbstractControl.GetVirtualWidth: Integer;
begin
  Result := FVirtWidth;
end;

function TAHMAbstractControl.GetVirtualHeight: Integer;
begin
  Result := FVirtHeight;
end;

procedure TAHMAbstractControl.SetVirtualWidth(Width: Integer);
begin
  FVirtWidth := Width;
  FWidth := ScreenInstance.ScaleVirtualX(Width);
end;

procedure TAHMAbstractControl.SetVirtualHeight(Height: Integer);
begin
  FVirtHeight := Height;
  FHeight := ScreenInstance.ScaleVirtualY(Height);
end;

function TAHMAbstractControl.GetDesignerPos: String;
begin
  // Return current X/Y position in concatenated string format
  Result := Format('%d,%d', [VirtualXPos, VirtualYPos]);
end;

procedure TAHMAbstractControl.SetDesignerPos(Position: String);
var
  X, Y: Integer;
begin
  // Parse concatenated string format to derive new X/Y position
  if ParseStringValues(Position, X, Y) then
  begin
    SetVirtualXPos(X);
    SetVirtualYPos(Y);
  end;
end;

function TAHMAbstractControl.GetDesignerRot: String;
begin
  // Return X/Y/Z rotation from associated placement
  if Assigned(FPlacement) then Result := FPlacement.DesignerRot
                          else Result := '';
end;

procedure TAHMAbstractControl.SetDesignerRot(Rotation: String);
begin
  // Apply X/Y/Z rotation to associated placement
  if Assigned(FPlacement) then FPlacement.DesignerRot := Rotation;
end;

function TAHMAbstractControl.GetVirtualXPos: Integer;
begin
  // This returns control position in virtual screen space
  Result := ScreenInstance.ScalePhysicalX(ClipRect.X);
end;

procedure TAHMAbstractControl.SetVirtualXPos(XPos: Integer);
begin
  // Update placement to specified position in virtual screen space
  if Assigned(FPlacement) then
    if Self is TAHMContainer then
      FPlacement.VirtualXPos := XPos
    else if Assigned(FViewport) then
      FPlacement.VirtualXPos := XPos - FViewport.VirtualXOrigin;
end;

function TAHMAbstractControl.GetVirtualYPos: Integer;
begin
  // This returns control position in virtual screen space
  Result := ScreenInstance.ScalePhysicalY(ClipRect.Y);
end;

procedure TAHMAbstractControl.SetVirtualYPos(YPos: Integer);
begin
  // Update placement to specified position in virtual screen space
  if Assigned(FPlacement) then
    if Self is TAHMContainer then
      FPlacement.VirtualYPos := YPos
    else if Assigned(FViewport) then
      FPlacement.VirtualYPos := YPos - FViewport.VirtualYOrigin;
end;

function TAHMAbstractControl.GetState: TAHMControlState;
begin
  // Abstract controls can only have two states - idle or hidden
  if FVisible then
    Result := csIdle
  else
    Result := csHidden;
end;

procedure TAHMAbstractControl.SetPlacement(Place: TAHMPlace);
begin
  // Invalidate size/position when we change placement
  if FPlacement <> Place then
  begin
    FPlacement := Place;
    InvalidateSize;
  end;
end;

procedure TAHMAbstractControl.SetEffect(Effect: TAHMEffect);
begin
  // Invalidate effect when we change effect
  if FEffect <> Effect then
  begin
    FEffect := Effect;
    InvalidateEffect;
  end;
end;

procedure TAHMAbstractControl.SetInitiallyVisible(InitiallyVisible: Boolean);
begin
  // Update runtime visibility when initial visibility changes
  if FInitiallyVisible <> InitiallyVisible then
  begin
    FInitiallyVisible := InitiallyVisible;
    FVisible := InitiallyVisible;
  end;
end;

procedure TAHMAbstractControl.InvalidateSize;
begin
  // Flag control to resize on next render
  FLoaded := False;
  FSized := False;

  // Descendants may implement more complex behaviour
end;

procedure TAHMAbstractControl.InvalidateEffect;
begin
  // Flag control to initialise effect on next render
  FInitialisedEffect := False;
end;

function TAHMAbstractControl.DesignerAdjustPos(DX, DY: Integer): Boolean;
begin
  // Default result - not modified
  Result := False;

  // If we don't yet have a position assigned then create one on the fly
  if ((DX <> 0) or (DY <> 0)) and (FPlacement = nil) then
    with ApplicationManagerInstance.ThemeManager.InterfaceDescriptor.PlaceList do
    begin
      Placement := AddPlace('', GenerateObjectName(Self.Name));
      Self.Modified := True;

       // Need to refresh in designer
      Result := True;
    end;

  // Update position in response to designer adjustment
  VirtualXPos := VirtualXPos + DX;
  VirtualYPos := VirtualYPos + DY;

  if (DX <> 0) or (DY <> 0) then
    if Placement <> nil then Placement.Modified := True
                        else Modified := True;

  // Force resize so that designer can record changes immediately
  Resize;
end;

function TAHMAbstractControl.DesignerAdjustRot(DX, DY, DZ: Single): Boolean;
begin
  // Default result - not modified
  Result := False;

  // If we don't yet have a position assigned then create one on the fly
  if ((DX <> 0) or (DY <> 0) or (DZ <> 0)) and (FPlacement = nil) then
    with ApplicationManagerInstance.ThemeManager.InterfaceDescriptor.PlaceList do
    begin
      Placement := AddPlace('', GenerateObjectName(Self.Name));
      Self.Modified := True;

       // Need to refresh in designer
      Result := True;
    end;

  // Update rotation in response to designer adjustment
  if Assigned(FPlacement) then
  begin
    FPlacement.RotationX := FPlacement.RotationX + DX;
    FPlacement.RotationY := FPlacement.RotationY + DY;
    FPlacement.RotationZ := FPlacement.RotationZ + DZ;
  end;

  if (DX <> 0) or (DY <> 0) or (DZ <> 0) then
    if Placement <> nil then Placement.Modified := True
                        else Modified := True;
end;

function TAHMAbstractControl.CanvasRect(X, Y, Width, Height: Single): TAHMRectF;
begin
  // Default implementation for control - relative to viewport
  if Assigned(FViewport) then
    Result := FViewport.ClientRect(X, Y, Width, Height)
  else
    Result := EmptyRectF;
end;

function TAHMAbstractControl.ClipRect: TAHMRectF;
begin
  // Bounding rectangle for control in screen coordinates
  if Assigned(FViewport) then
    if FViewport.Clipping then
      Result := FViewport.ClippedClientRect(FXPos, FYPos, FWidth, FHeight)
    else
      Result := FViewport.ClientRect(FXPos, FYPos, FWidth, FHeight)
  else
    Result := EmptyRectF;
end;

procedure TAHMAbstractControl.MoveControl(XPos, YPos: Single);
begin
  // Change control coordinates. Descendants may do other stuff if required
  FXPos := XPos;
  FYPos := YPos;
end;

procedure TAHMAbstractControl.SizeControl(Width, Height: Single);
begin
  // Change control size. Descendants may do other stuff if required
  FWidth := Width;
  FHeight := Height;
end;

procedure TAHMAbstractControl.RotateControl(XRot, YRot, ZRot: Single);
begin
  // Change control rotation. Descendants may do other stuff if required
  FXRot := XRot;
  FYRot := YRot;
  FZRot := ZRot;
end;

function TAHMAbstractControl.BoundsPoint(XPos, YPos: Single): Boolean;
var
  W, H: Single;
begin
  // Get control width & height
  W := FWidth;
  H := FHeight;

  // Account for viewport position
  if Assigned(FViewport) then
  begin
    XPos := XPos - FViewport.XOrigin;
    YPos := YPos - FViewport.YOrigin;

    // If width or height are zero use viewport size
    if W = 0 then W := FViewport.Width;
    if H = 0 then H := FViewport.Height;
  end;

  // Check whether passed point is within control bounds
  Result := (XPos >= FXPos) and (YPos >= FYPos) and
            (XPos <= FXPos + W) and (YPos <= FYpos + H);
end;

function TAHMAbstractControl.PageScroll(Direction: TAHMDirection): Boolean;
begin
  // Descendants may override this to handle page scrolling
  Result := False;
end;

function TAHMAbstractControl.Scroll(Direction: TAHMDirection): Boolean;
begin
  // Descendants may override this to handle scrolling
  Result := False;
end;

function TAHMAbstractControl.SmoothScroll(DX, DY: Single): Boolean;
begin
  // Descendants may override this to handle smooth scrolling
  Result := False;
end;

function TAHMAbstractControl.RenderPreview: Boolean;
{$IFDEF DESIGNER}
var
  OldVisible: Boolean;
{$ENDIF}
begin
{$IFDEF DESIGNER}
  // Render control to screen viewport
  with ScreenInstance.Viewport do
  begin
    Canvas.DesignerBackground(ViewportRect);

    // Force control visibility for preview rendering
    OldVisible := Visible;
    Visible := True;

    // Update border visibility for controls
    if Self is TAHMControl then
      TAHMControl(Self).DrawBorder := ApplicationManagerInstance.UIManager.DrawControlBorders;

    Render(ScreenInstance.Viewport);
    Visible := OldVisible;
  end;
{$ENDIF}
  Result := True;
end;

procedure TAHMAbstractControl.Invalidate;
begin
  // Invalidate control - it will need to be re-rendered
  FValid := False;
  FLoaded := False;
end;

procedure TAHMAbstractControl.InitialiseEffect;
begin
  // Apply scaling & gamma effects to this control
  if FEffect <> nil then
  begin
    FOwnXScale := FEffect.XScale;
    FOwnYScale := FEffect.YScale;
    if Assigned(FEffect.Gamma) then
      FOwnGamma.Assign(FEffect.Gamma.Colour)
    else
      FOwnGamma.Assign(TAHMCanvasColour.White);
  end
  else
  begin
    FOwnXScale := 1.0;
    FOwnYScale := 1.0;
    FOwnGamma.Assign(TAHMCanvasColour.White);
  end;

  // Effect is now initialised
  FInitialisedEffect := True;
end;

procedure TAHMAbstractControl.Rendering;
begin
  // Initialise control if this is first time we get rendered
  if not FLoaded then Loaded;

  // Initialise effects for this control if necessary
  if not FInitialisedEffect then InitialiseEffect;
end;

procedure TAHMAbstractControl.Rendered;
begin
  // Descendants may perform any post render operations here
end;

procedure TAHMAbstractControl.Render(Viewport: TAHMViewport);
begin
  // Descendants may override this to perform rendering
end;

procedure TAHMAbstractControl.Loaded;
begin
  // Adjust size & position if not already done so
  if not FSized then Resize;

  if LogDebugEnabled then LogDebug('Loaded control ' + Name);

  // Set flag to indicate we're loaded - descendants can do something
  // more useful here if required
  FLoaded := True;
end;

procedure TAHMAbstractControl.ClearProperties;
begin
  inherited;

  // Clear custom control properties
  VirtualWidth := 0;
  VirtualHeight := 0;
  FEffect := nil;
  FDepth := dtMidground;
  FPlacement := nil;
  FOwnXScale := 1.0;
  FOwnYScale := 1.0;
  FVisible := True;
  FInitiallyVisible := True;
end;

procedure TAHMAbstractControl.Resized;
begin
  // Okay, we've adjusted size & position now
  FSized := True;
end;

procedure TAHMAbstractControl.Assign(Source: TPersistent);
begin
  inherited Assign(Source);

  with Source as TAHMAbstractControl do
  begin
    Self.Effect := Effect;
    Self.Depth := Depth;
    Self.Placement := Placement;
    Self.InitiallyVisible := InitiallyVisible;
    Self.Visible := Visible;

    // Properties have changed, so we may need to reinitialise
    FLoaded := False;
  end;
end;

procedure TAHMAbstractControl.ParseCommonXML(XML: TAHMXMLHelper);
var
  sName: String;
begin
  // Parse common properties for all control descendants
  with ApplicationManagerInstance.ThemeManager do
    if XML.CurrentElement = TAG_PLACEMENT then
    begin
      sName := XML.GetAttribute(ATTR_NAME);
      Placement := InterfaceDescriptor.PlaceList.GetPlace(sName);
    end
    else if XML.CurrentElement = TAG_DEPTH then
      Depth := Str2Depth(XML.GetValue)
    else if XML.CurrentElement = TAG_EFFECT then
    begin
      sName := XML.GetAttribute(ATTR_NAME);
      if sName <> '' then Effect :=
        VisualDescriptor.EffectList.GetEffect(sName);
    end;
end;

procedure TAHMAbstractControl.BuildProperties(Properties: TAHMUIPropertyList);
begin
{$IFDEF DESIGNER}
  inherited;

  // Build custom abstract control properties
  Properties.Add('Visible', 'InitiallyVisible', CAT_APPEARANCE, HINT_VISIBLE);
  Properties.Add('Placement', 'Placement', CAT_DESIGN, HINT_PLACEMENT);
  Properties.Add('Depth', 'Depth', CAT_DESIGN, HINT_DEPTH);
  Properties.Remove('OwnGamma');
{$ENDIF}
end;

//----------------------------------------------------------------------------//

constructor TAHMControl.Create(const Name: String);
begin
  inherited Create(Name);

  ObjectType := otControls;
  
  FLayoutLocn := TAHMLayoutLocation.Create;
end;

destructor TAHMControl.Destroy;
begin
  FLayoutLocn.Free;
  FScroller.Free;

  inherited Destroy;
end;

function TAHMControl.TagBegin: String;
begin
  // We'll always need a begin tag and name attribute
  Result := TGB + XmlTagName + SPC + ATTR_NAME + ATB + Name;

  // Do we have an ancestor assigned?
  if (Ancestor <> nil) then
  begin
    // We need style attribute if it's different from ancestor
    if (FStyle <> nil) and (FStyle <> TAHMControl(Ancestor).Style) then
      Result := Result + ATN + ATTR_STYLE + ATB + FStyle.Name;
    // We need description attribute if it's different from ancestor
    if (Description <> '') and (Description <> Ancestor.Description) then
      Result := Result + ATN + ATTR_DESC + ATB + Description;
    // Add on ancestor name attribute
    Result := Result + ATN + ATTR_INHERIT + ATB + Ancestor.Name + ATC;
  end
  else
  begin
    // We need style attribute if it's not empty
    if (FStyle <> nil) then
      Result := Result + ATN + ATTR_STYLE + ATB + FStyle.Name;
    // We need description attribute if it's not empty
    if (Description <> '') then
      Result := Result + ATN + ATTR_DESC + ATB + Description;
    Result := Result + ATC;
  end;
end;

function TAHMControl.GetGamma: TAHMCanvasColour;
begin
  // Apply container gamma correction if we have one
  if FViewport.Owner is TAHMContainer then
  begin
    FGamma.Pixel := FOwnGamma.GammaCorrect(TAHMContainer(FViewport.Owner).Gamma);
    Result := FGamma;
  end
  else
    Result := FOwnGamma;
end;

function TAHMControl.GetXScale: Single;
begin
  // Apply container horizontal scale correction if we have one
  if Assigned(FViewport) and (FViewport.Owner is TAHMContainer) then
    Result := FOwnXScale * TAHMContainer(FViewport.Owner).XScale
  else
    Result := FOwnXScale;
end;

function TAHMControl.GetYScale: Single;
begin
  // Apply container vertical scale correction if we have one
  if Assigned(FViewport) and (FViewport.Owner is TAHMContainer) then
    Result := FOwnYScale * TAHMContainer(FViewport.Owner).YScale
  else
    Result := FOwnYScale;
end;

function TAHMControl.GetVirtualWidth: Integer;
begin
  // If we have a style assigned then return width of that instead
  if Assigned(FStyle) then
    Result := FStyle.VirtualWidth
  else
    Result := inherited GetVirtualWidth;
end;

function TAHMControl.GetVirtualHeight: Integer;
begin
  // If we have a style assigned then return height of that instead
  if Assigned(FStyle) then
    Result := FStyle.VirtualHeight
  else
    Result := inherited GetVirtualHeight;
end;

procedure TAHMControl.SetVirtualWidth(Width: Integer);
begin
  // If we have a style assigned then apply change to that instead
  if Assigned(FStyle) then
    FStyle.VirtualWidth := Width
  else
    inherited;
end;

procedure TAHMControl.SetVirtualHeight(Height: Integer);
begin
  // If we have a style assigned then apply change to that instead
  if Assigned(FStyle) then
    FStyle.VirtualHeight := Height
  else
    inherited;
end;

function TAHMControl.GetDesignerWidth: Integer;
begin
  // Retrieve virtual style width for designer if assigned
  Result := GetVirtualWidth;
end;

function TAHMControl.GetDesignerHeight: Integer;
begin
  // Retrieve virtual style height for designer if assigned
  Result := GetVirtualHeight;
end;

function TAHMControl.GetDesignerPos: String;
begin
  // Retrieve layout position for control if we have one
  if Assigned(Location.Layout) then
    Result := Location.DesignerPos
  else
    Result := inherited GetDesignerPos;
end;

procedure TAHMControl.SetDesignerPos(Position: String);
begin
  // Update layout position for control if we have one
  if Assigned(Location.Layout) then
    Location.DesignerPos := Position
  else
    inherited SetDesignerPos(Position);
end;

function TAHMControl.GetDesignerSize: String;
begin
  // Return current width/height in concatenated string format
  Result := Format('%d,%d', [VirtualWidth, VirtualHeight])
end;

procedure TAHMControl.SetDesignerSize(Size: String);
var
  W, H: Integer;
begin
  if ParseStringValues(Size, W, H) then
  begin
    VirtualWidth := W;
    VirtualHeight := H;
  end;
end;

function TAHMControl.GetLayout: TAHMLayout;
begin
  Result := FLayoutLocn.Layout;
end;

procedure TAHMControl.SetLayout(Layout: TAHMLayout);
begin
  FLayoutLocn.Layout := Layout;
end;

function TAHMControl.GetLayoutRow: Integer;
begin
  Result := FLayoutLocn.Row;
end;

procedure TAHMControl.SetLayoutRow(Row: Integer);
begin
  FLayoutLocn.Row := Row;
end;

function TAHMControl.GetLayoutCol: Integer;
begin
  Result := FLayoutLocn.Column;
end;

procedure TAHMControl.SetLayoutCol(Col: Integer);
begin
  FLayoutLocn.Column := Col;
end;

function TAHMControl.GetLayoutPos: Integer;
begin
  Result := FLayoutLocn.Position;
end;

procedure TAHMControl.SetLayoutPos(Pos: Integer);
begin
  FLayoutLocn.Position := Pos;
end;

function TAHMControl.GetDisplayMeta: String;
begin
  // Get meta data value to use for display text
  Result := ApplicationManagerInstance.MetaDataManager.GetMetaValue(FMetaValue);
end;

function TAHMControl.GetDisplayText: String;
begin
  // Use cached display text if set
  if FDisplayText <> '' then
    Result := FDisplayText
  else if FMetaValue <> '' then
  begin
    // We have a meta data value set, so lookup its current value
    Result := GetDisplayMeta;
    // If we have a meta group assigned, then use value as index for lookup
    if FMetaGroup <> '' then
      Result := ApplicationManagerInstance.MetaDataManager.LookupMetaValue(Result, FMetaGroup);
  end
  else
  begin
    // No meta data, lookup text translation for stored text
    Result :=  LanguageManagerInstance.Translate(FText);
  end;
end;

procedure TAHMControl.SetText(NewText: TAHMTextString);
begin
  // Descendant controls may override this to do something more complex
  FText := NewText;
  FDisplayText := '';
end;

procedure TAHMControl.SetStyleName(Name: String);
var
  StyleObjects: TAHMUIObjectList;
begin
  // Determine appropriate style list
  StyleObjects := GetStyles;
  if Assigned(StyleObjects) then
    Style := TAHMUIDisplayObject(StyleObjects.GetObject(Name))
  else
    Style := nil;

  // Descendants may override this to perform additional setup tasks
end;

procedure TAHMControl.SetStyle(Style: TAHMUIDisplayObject);
begin
  if FStyle <> Style then
  begin
    // Invalidate style on change
    FStyle := Style;
    InvalidateStyle;
  end;
end;

function TAHMControl.GetDefaultStyle: TAHMUIDisplayObject;
var
  Styles: TAHMUIObjectList;
begin
  // Use custom style if one is assigned
  if Assigned(FStyle) then
    Result := FStyle
  else if Assigned(FDefaultStyle) then
    Result := FDefaultStyle
  else
  begin
    // Lookup associated styles for this control
    Styles := GetStyles;

    // Verify styles supports an appropriate default object
    if Assigned(Styles) and (Styles.DefaultObject is TAHMUIDisplayObject) then
      Result := TAHMUIDisplayObject(Styles.DefaultObject)
    else
      Result := nil;

    // Cache default style for next time
    FDefaultStyle := Result;
  end;
end;

function TAHMControl.GetStyles: TAHMUIObjectList;
begin
  // Default implementation returns class method
  Result := TAHMControlClass(Self.ClassType).GetStyleList;
end;

function TAHMControl.GetOverlayRect: TAHMRectF;
begin
  // Build standard rectangle for overlaying bitmaps
  Result := MakeRectF(FXPos, FYPos, FWidth, FHeight);
end;

procedure TAHMControl.InvalidateStyle;
begin
  // Flag control to refresh style on next render
  InvalidateSize;
  FValid := False;

  // Descendants may implement more complex behaviour
end;

procedure TAHMControl.ClearProperties;
begin
  inherited;

  // Clear custom control properties
  FText := '';
  FMetaValue := '';
  FMetaGroup := '';
  FMetaIndex := '';
  FLayoutLocn.Clear;
  FStyleName := '';
  FStyle := nil;
end;

procedure TAHMControl.RenderEffects;
var
  Overlay: TAHMRectF;
begin
  // Render idle effect overlay
  if Assigned(FEffect) and Assigned(FEffect.Bitmap) then
  begin
    Overlay := GetOverlayRect;
    FEffect.Bitmap.Render(Self, Overlay.X, Overlay.Y, Overlay.Width, Overlay.Height, True);
  end;
end;

function TAHMControl.DesignerAdjustPos(DX, DY: Integer): Boolean;
begin
  // Update layout for control if we have one
  if Assigned(Location.Layout) then
  begin
    // Update control layout - allow it to update associated controls
    if Location.AdjustLayout(DX, DY) then
      Location.Layout.Modified := True;
    Result := False;
  end
  else
    // Use standard behaviour if no layout
    Result := inherited DesignerAdjustPos(DX, DY);
end;

function TAHMControl.DesignerAdjustRot(DX, DY, DZ: Single): Boolean;
begin
  Result := inherited DesignerAdjustRot(DX, DY, DZ);

  // Invalidate layout to allow it to reposition controls
  if Assigned(Location.Layout) then
    ApplicationManagerInstance.UIManager.InvalidateUIObject(Location.Layout);
end;

function TAHMControl.DesignerAdjustSize(DX, DY: Integer): Boolean;
var
  NewWidth, NewHeight: Integer;
begin
  // Update control/style dimensions - clamp negative values
  NewWidth := Max(0, VirtualWidth + DX);
  NewHeight := Max(0, VirtualHeight + DY);
  if DX <> 0 then VirtualWidth := NewWidth;
  if DY <> 0 then VirtualHeight := NewHeight;

  if (DX <> 0) or (DY <> 0) then
    if Assigned(Style) then Style.Modified := True
                       else Modified := True;

  Result := False;
end;

function TAHMControl.ModifyActions: Integer;
begin
  // Control only supports editing text
  Result := 1;
end;

function TAHMControl.Modify(Action: Integer): Boolean;
begin
{$IFDEF DESIGNER}
  // Use standard modify method to edit text value
  with LanguageManagerInstance do
  begin
    Result := Languages[NativeLanguageName].ModifyValue(String(FText), CAPTION_TEXT);
    if Result then FDisplayText := '';
  end;

  if Result then DoModified;
{$ELSE}
  Result := False;
{$ENDIF}
end;

function TAHMControl.ModifyCaption(Action: Integer): String;
begin
{$IFDEF DESIGNER}
  Result := MODIFY_TEXT;
{$ELSE}
  Result := '';
{$ENDIF}
end;

procedure TAHMControl.InvalidateUIObject(UIObject: TAHMUIObject);
begin
  // Determine appropriate type of UIObject change here...

  if (UIObject is TAHMPlace) and (FPlacement = UIObject) then
  begin
    // Handle placement changes
    InvalidateSize;
  end
  else if (UIObject is TAHMLayout) and (FLayoutLocn.Layout = UIObject) then
  begin
    // Handle layout changes
    InvalidateSize;
  end
  else if (UIObject is TAHMUIDisplayObject) and (FStyle = UIObject) then
  begin
    // Handle style changes
    InvalidateStyle;
  end
  else if (UIObject is TAHMEffect) and (FEffect = UIObject) then
  begin
    // Handle effect changes
    InvalidateEffect;
  end;
end;

procedure TAHMControl.Render(Viewport: TAHMViewport);
begin
  // Keep a copy of passed viewport object
  FViewport := Viewport;

  // Apply rotation transformation to control
  with Viewport.Canvas do
    if (FXRot <> 0) or (FYRot <> 0) or (FZRot <> 0) then
      SetRotation(FXRot, FYRot, FZRot, FCentreX, FCentreY)
    else
      UnsetRotation;

  // Call rendering method to initialise control
  Rendering;

  // Perform standard control rendering
  if Visible then
  begin
    // Render control using associated style
    if Assigned(DefaultStyle) then DefaultStyle.Render(Self);

    // Render overlay effects
    RenderEffects;

    // Render borders for display in designer
    if DrawBorder then
      with ScreenInstance.Viewport.Canvas do
      begin
        PenWidth := 0.004;
        PenColour := TAHMCanvasColour.Black;
        Rectangle(ClipRect, Gamma, XScale, YScale);
        PenWidth := 0.002;
        PenColour := TAHMCanvasColour.White;
        Rectangle(ClipRect, Gamma, XScale, YScale);
      end;
  end;

  // Perform any post rendering tasks
  Rendered;
end;

procedure TAHMControl.Resize;
begin
  // Call inherited method to set resized flag
  inherited Resized;

  // Take default size from our derived style object
  if Assigned(Style) then
  begin
    if Style.Width <> 0 then FWidth := Style.Width;
    if Style.Height <> 0 then FHeight := Style.Height;
  end;

  // Determine control position and sizing
  if Assigned(FLayoutLocn.Layout) then
  begin
    // We have a layout location assigned so use that to set position instead
    FLayoutLocn.AllocatePosition(FXPos, FYPos);
  end
  else if Assigned(FPlacement) then
  begin
    // Use placement object to apply position and size
    with FViewport do
      FPlacement.ApplyPosition(FXPos, FYPos, FWidth, FHeight, Width, Height);
  end
  else
  begin
    // No placement, so our position defaults to the origin
    FXPos := 0;
    FYPos := 0;
  end;

  // Determine control flipping & rotation
  if Assigned(FPlacement) then
  begin
    // Determine vertical/horizontal flipping
    FXFlip := FPlacement.HorizontalFlip;
    FYFlip := FPlacement.VerticalFlip;

    // Determine rotation
    FXRot := FPlacement.RotationX;
    FYRot := FPlacement.RotationY;
    FZRot := FPlacement.RotationZ;
  end
  else
  begin
    // Reset flip & rotation properties if no place assigned
    FXFlip := False;
    FYFlip := False;
    FXRot := 0;
    FYRot := 0;
    FZRot := 0;
  end;

  // Calculate absolute midpoint of our object
  if Assigned(FViewport) then
  begin
    FNavigationX := FViewport.XOrigin + FXPos;
    FNavigationY := FViewport.YOrigin + FYPos;
    FCentreX := FViewport.XOrigin + FXPos + (FWidth / 2);
    FCentreY := FViewport.YOrigin + FYPos + (FHeight / 2);
  end
  else
  begin
    FNavigationX := FXPos;
    FNavigationY := FYPos;
    FCentreX := FXPos + (FWidth / 2);
    FCentreY := FYPos + (FHeight / 2);
  end;
  
  // Cache initial position and dimensions
  FInitXPos := FXPos;
  FInitYPos := FYPos;
  FInitWidth := FWidth;
  FInitHeight := FHeight;
end;

procedure TAHMControl.SeedObject;
begin
{$IFDEF DESIGNER}
  if Ancestor = nil then
  begin
    // Add default display text for control
    DisplayText := DEFAULT_CTRL_TEXT;

    // Set a default size for this control if none specified
    if (DefaultStyle = nil) or (DefaultStyle.VirtualWidth = 0) or (DefaultStyle.VirtualHeight = 0) then
    begin
      VirtualWidth := DEFAULT_CTRL_WIDTH;
      VirtualHeight := DEFAULT_CTRL_HEIGHT;
    end
    else
    begin
      VirtualWidth := DefaultStyle.VirtualWidth;
      VirtualHeight := DefaultStyle.VirtualHeight;
    end;
  end;
{$ENDIF}
end;

procedure TAHMControl.Assign(Source: TPersistent);
begin
  inherited Assign(Source);

  with Source as TAHMControl do
  begin
    Self.Text := Text;
    if Assigned(Self.Scroller) then Self.Scroller.Assign(Scroller);
    Self.Location.Assign(Location);
    Self.Style := Style;
    Self.MetaValue := MetaValue;
    Self.MetaGroup := MetaGroup;
    Self.MetaIndex := MetaIndex;
  end;
end;

procedure TAHMControl.SaveAsXML(Lines: TStrings);
var
  oAncs: TAHMControl;
  sWork: String;
begin
  // Determine which ancestor to compare against - real or empty
  oAncs := TAHMControl(ObjectToCompare);

  // We need to explicitly save comments in overridden method
  SaveXMLComments(Lines);

  // Only populate attributes that differ from our ancestor
  Lines.Add(TABS[2] + TagBegin);

  // Do we need to store size? (only if no style is assigned)
  if (FStyle = nil) and
     (((FVirtWidth <> 0) and (FVirtWidth <> oAncs.VirtualWidth)) or
     ((FVirtHeight <> 0) and (FVirtHeight <> oAncs.VirtualHeight))) then
    Lines.Add(TABS[3] + TGB+TAG_SIZE+TGC+ IntToStr(FVirtWidth) + ',' +
              IntToStr(FVirtHeight) + TGE+TAG_SIZE+TGC);

  // Do we need to store text?
  if (FText <> '') and (FText <> oAncs.Text) then
    Lines.Add(TABS[3] + TGB+TAG_TEXT+SPC+ATTR_NAME+ATB + EncodeXML(FText) + ATE);

  // Do we need meta data element?
  if ((FMetaValue <> '') and (FMetaValue <> oAncs.MetaValue)) or
     ((FMetaGroup <> '') and (FMetaGroup <> oAncs.MetaGroup)) or
     ((FMetaIndex <> '') and (FMetaIndex <> oAncs.MetaIndex)) then
  begin
    sWork := TGB+TAG_META_VALUE;
    if (FMetaValue <> '') and (FMetaValue <> oAncs.MetaValue) then
      sWork := sWork + SPC+ATTR_NAME+ATB + EncodeXML(FMetaValue) + ATQ;
    if (FMetaGroup <> '') and (FMetaGroup <> oAncs.MetaGroup) then
      sWork := sWork + SPC+ATTR_GROUP+ATB + EncodeXML(FMetaGroup) + ATQ;
    if (FMetaIndex <> '') and (FMetaIndex <> oAncs.MetaIndex) then
      sWork := sWork + SPC+ATTR_INDEX+ATB + EncodeXML(FMetaIndex) + ATQ;
    Lines.Add(TABS[3] + sWork + ETE);
  end;

  // Do we need depth element?
  if (FDepth <> dtMidground) and (FDepth <> oAncs.Depth) then
    Lines.Add(TABS[3] + TGB+TAG_DEPTH+TGC + Depth2Str(FDepth) + TGE+TAG_DEPTH+TGC);

  // Do we need placement element?
  if (FPlacement <> nil) and (FPlacement <> oAncs.Placement) then
    Lines.Add(TABS[3] + FPlacement.XMLReference(TAG_PLACEMENT));

  // Do we need location element?
  with FLayoutLocn do
    if (Layout <> nil) and (not Equals(oAncs.Location)) then
    begin
      sWork := TGB+TAG_LAYOUT+SPC+ATTR_NAME+ATB + EncodeXML(Layout.Name) + ATQ;
      if Position <> 0 then
        sWork := sWork + SPC+ATTR_POSITION+ATB + IntToStr(Position) + ATQ;
      if Row <> 0 then
        sWork := sWork + SPC+ATTR_ROW+ATB + IntToStr(Row) + ATQ;
      if Column <> 0 then
        sWork := sWork + SPC+ATTR_COL+ATB + IntToStr(Column) + ATQ;
      Lines.Add(TABS[3] + sWork + ETE);
    end;

  // Do we need status element?
  if (not FInitiallyVisible) or (FInitiallyVisible <> oAncs.InitiallyVisible) then
  begin
    sWork := TGB+TAG_STATUS+SPC+ATTR_VISIBLE+ATB + Bool2Str(FInitiallyVisible);
    Lines.Add(TABS[3] + sWork + ATE);
  end;

  // Do we need effect element?
  if (FEffect <> nil) and (FEffect <> oAncs.Effect) then
    Lines.Add(TABS[3] + FEffect.XMLReference);

  Lines.Add(TABS[2] + TagEnd);
end;

procedure TAHMControl.ParseFromXML(XML: TAHMXMLHelper);
var
  sStyle, sName, sGroup, sIndex, sPosition: String;
  sRow, sCol, sVisible, sEnabled, sMouse: String;
  ControlNode, PropsNode: TAHMXMLNode;
  iX, iY: Integer;
begin
  // Initialise parsing nodes
  ControlNode := XML.CurrentNode;
  PropsNode := nil;

  // Populate style property if present
  sStyle := XML.GetAttribute(ATTR_STYLE);
  if sStyle <> '' then
  begin
    SetStyleName(sStyle);
    if Style = nil then
      LogParsingError(THM_PRS_INT_UNKNOWN_CONTROL_STYLE + sStyle, Self);
  end;

  // Discover additional properties for this object
  with ApplicationManagerInstance.ThemeManager.InterfaceDescriptor do
    while XML.NextElement(ControlNode, PropsNode) do
      if XML.CurrentElement = TAG_TEXT then
      begin
        Text := XML.GetAttribute(ATTR_NAME);
      end
      else if XML.CurrentElement = TAG_META_VALUE then
      begin
        sName := XML.GetAttribute(ATTR_NAME);
        sGroup := XML.GetAttribute(ATTR_GROUP);
        sIndex := XML.GetAttribute(ATTR_INDEX);
        if sName <> '' then MetaValue := sName;
        if sGroup <> '' then MetaGroup := sGroup;
        if sIndex <> '' then MetaIndex := sIndex;
      end
      else if XML.CurrentElement = TAG_SIZE then
      begin
        if XML.GetCoordsValue(iX, iY) then
        begin
          VirtualWidth := iX;
          VirtualHeight := iY;
        end
        else
          LogParsingError(THM_PRS_INT_MALFORMED_CONTROL_SIZE_TAGS, Self);
      end
      else if XML.CurrentElement = TAG_LAYOUT then
      begin
        sName := XML.GetAttribute(ATTR_NAME);
        sPosition := XML.GetAttribute(ATTR_POSITION);
        sRow := XML.GetAttribute(ATTR_ROW);
        sCol := XML.GetAttribute(ATTR_COL);
        with Location do
        begin
          Layout := LayoutList.GetLayout(sName);
          if sPosition <> '' then Position := StrToInt(sPosition);
          if sRow <> '' then Row := StrToInt(sRow);
          if sCol <> '' then Column := StrToInt(sCol);
        end;
      end
      else if XML.CurrentElement = TAG_NAVIGATION then
      begin
        if Self is TAHMInteractiveControl then
        begin
          sName := XML.GetAttribute(ATTR_UP);
          if sName <> '' then TAHMInteractiveControl(Self).NavigateUpName := sName;
          sName := XML.GetAttribute(ATTR_DOWN);
          if sName <> '' then TAHMInteractiveControl(Self).NavigateDownName := sName;
          sName := XML.GetAttribute(ATTR_LEFT);
          if sName <> '' then TAHMInteractiveControl(Self).NavigateLeftName := sName;
          sName := XML.GetAttribute(ATTR_RIGHT);
          if sName <> '' then TAHMInteractiveControl(Self).NavigateRightName := sName;
        end
        else
          LogParsingError(THM_PRS_INT_UNSUPPORTED_CONTROL_TAG + TAG_NAVIGATION, Self);
      end
      else if XML.CurrentElement = TAG_FOCUSED then
      begin
        if Self is TAHMInteractiveControl then
        begin
          sName := XML.GetAttribute(ATTR_NAME);
          if sName <> '' then TAHMInteractiveControl(Self).FocusedEffect :=
          ApplicationManagerInstance.ThemeManager.VisualDescriptor.EffectList.GetEffect(sName);
        end
        else
          LogParsingError(THM_PRS_INT_UNSUPPORTED_CONTROL_TAG + TAG_FOCUSED, Self);
      end
      else if XML.CurrentElement = TAG_PRESSED then
      begin
        if Self is TAHMInteractiveControl then
        begin
          sName := XML.GetAttribute(ATTR_NAME);
          if sName <> '' then TAHMInteractiveControl(Self).PressedEffect :=
          ApplicationManagerInstance.ThemeManager.VisualDescriptor.EffectList.GetEffect(sName);
        end
        else
          LogParsingError(THM_PRS_INT_UNSUPPORTED_CONTROL_TAG + TAG_PRESSED, Self);
      end
      else if XML.CurrentElement = TAG_CHECKED then
      begin
        if Self is TAHMInteractiveControl then
        begin
          sName := XML.GetAttribute(ATTR_NAME);
          if sName <> '' then TAHMInteractiveControl(Self).CheckedEffect :=
          ApplicationManagerInstance.ThemeManager.VisualDescriptor.EffectList.GetEffect(sName);
        end
        else
          LogParsingError(THM_PRS_INT_UNSUPPORTED_CONTROL_TAG + TAG_CHECKED, Self);
      end
      else if XML.CurrentElement = TAG_HINT then
      begin
        sName := XML.GetAttribute(ATTR_NAME);
        if Self is TAHMInteractiveControl then
          TAHMInteractiveControl(Self).Hint := sName
          else
            LogParsingError(THM_PRS_INT_UNSUPPORTED_CONTROL_TAG + TAG_HINT, Self);
      end
      else if XML.CurrentElement = TAG_ACTIONS then
      begin
        if not (Self is TAHMInteractiveControl) then
          LogParsingError(THM_PRS_INT_UNSUPPORTED_CONTROL_TAG + TAG_ACTIONS, Self)
        else
          TAHMInteractiveControl(Self).Actions.ParseFromXML(XML);
      end
      else if XML.CurrentElement = TAG_STATUS then
      begin
        sVisible := XML.GetAttribute(ATTR_VISIBLE);
        sEnabled := XML.GetAttribute(ATTR_ENABLED);
        sMouse   := XML.GetAttribute(ATTR_MOUSE);
        if sVisible <> '' then
          InitiallyVisible := Str2Bool(sVisible);
        if sEnabled <> '' then
          if Self is TAHMInteractiveControl then
            TAHMInteractiveControl(Self).InitiallyEnabled := Str2Bool(sEnabled)
          else
            LogParsingError(THM_PRS_INT_UNSUPPORTED_CONTROL_TAG + ATTR_ENABLED, Self);
        if sMouse <> '' then
          if Self is TAHMInteractiveControl then
            TAHMInteractiveControl(Self).InitiallyMouseEnabled := Str2Bool(sMouse)
          else
            LogParsingError(THM_PRS_INT_UNSUPPORTED_CONTROL_TAG + ATTR_MOUSE, Self);
      end
      else
      begin
        // Handle common XML tags to all control descendants
        ParseCommonXML(XML);
      end;
end;

procedure TAHMControl.BuildProperties(Properties: TAHMUIPropertyList);
begin
{$IFDEF DESIGNER}
  inherited;

  // Build custom control properties
  Properties.Add('Effect', 'Effect', CAT_APPEARANCE, HINT_EFFECT);
  Properties.Add('Style', 'Style', CAT_APPEARANCE, HINT_STYLE);
  Properties.Add('Text', 'Text', CAT_CONTENT, HINT_TEXT);
  Properties.Add('Meta Value', 'MetaValue', CAT_CONTENT, HINT_METAVALUE);
  Properties.Add('Meta Group', 'MetaGroup', CAT_CONTENT, HINT_METAGROUP);
  Properties.Add('Meta Index', 'MetaIndex', CAT_CONTENT, HINT_METAINDEX);
  Properties.Add('Layout', 'Layout', CAT_DESIGN, HINT_LAYOUT);
  Properties.Add('Layout Row', 'LayoutRow', CAT_DESIGN, HINT_LAYOUTROW);
  Properties.Add('Layout Column', 'LayoutColumn', CAT_DESIGN, HINT_LAYOUTCOL);
  Properties.Add('Layout Index', 'LayoutPosition', CAT_DESIGN, HINT_LAYOUTPOS);
  Properties.Add('Width', 'VirtualWidth', CAT_DESIGN, HINT_WIDTH);
  Properties.Add('Height', 'VirtualHeight', CAT_DESIGN, HINT_HEIGHT);
{$ENDIF}
end;

class function TAHMControl.GetStyleList: TAHMUIObjectList;
begin
  // Descendants may override this to provide appropriate style list
  Result := nil;
end;

//----------------------------------------------------------------------------//

constructor TAHMInteractiveControl.Create(const Name: String);
begin
  inherited Create(Name);

  // Set default properties
  FEnabled := True;
  FInitiallyEnabled := True;
  FMouseEnabled := True;
  FInitiallyMouseEnabled := True;
  FActions := TAHMActionList.Create;
  FActions.Owner := Self;
  FIdleTimeout := DefaultIdleTimeout;
  IsPreviewable := True;
end;

destructor TAHMInteractiveControl.Destroy;
begin
  FActions.FreeWithContents;
  inherited Destroy;
end;

procedure TAHMInteractiveControl.SetInitiallyEnabled(InitiallyEnabled: Boolean);
begin
  // Update runtime enabled status when initial enabled changes
  if FInitiallyEnabled <> InitiallyEnabled then
  begin
    FInitiallyEnabled := InitiallyEnabled;
    FEnabled := InitiallyEnabled;
  end;
end;

procedure TAHMInteractiveControl.SetInitiallyMouseEnabled(InitiallyMouseEnabled: Boolean);
begin
  // Update runtime mouse enabled status when initial mouse enabled changes
  if FInitiallyMouseEnabled <> InitiallyMouseEnabled then
  begin
    FInitiallyMouseEnabled := InitiallyMouseEnabled;
    FEnabled := InitiallyMouseEnabled;
  end;
end;

function TAHMInteractiveControl.GetChecked: Boolean;
begin
  // Default behaviour - just return value of boolean flag
  Result := FChecked;
end;

procedure TAHMInteractiveControl.SetChecked(Checked: Boolean);
begin
  // Default behaviour - just update internal boolean flag
  if FChecked <> Checked then
  begin
    FChecked := Checked;
    InvalidateEffect;
  end;
end;

procedure TAHMInteractiveControl.SetFocused(Focused: Boolean);
begin
  // Default behaviour - just update internal boolean flag
  if FFocused <> Focused then
  begin
    FFocused := Focused;
    InvalidateEffect;
  end;
end;

procedure TAHMInteractiveControl.SetFocusedEffect(Effect: TAHMEffect);
begin
  // Invalidate effect when we change effect
  if FFocusedEffect <> Effect then
  begin
    FFocusedEffect := Effect;
    InvalidateEffect;
  end;
end;

procedure TAHMInteractiveControl.SetPressedEffect(Effect: TAHMEffect);
begin
  // Invalidate effect when we change effect
  if FPressedEffect <> Effect then
  begin
    FPressedEffect := Effect;
    InvalidateEffect;
  end;
end;

procedure TAHMInteractiveControl.SetCheckedEffect(Effect: TAHMEffect);
begin
  // Invalidate effect when we change effect
  if FCheckedEffect <> Effect then
  begin
    FCheckedEffect := Effect;
    InvalidateEffect;
  end;
end;

function TAHMInteractiveControl.GetNavigateUp: TAHMInteractiveControl;
begin
  with ApplicationManagerInstance.ThemeManager.InterfaceDescriptor do
    Result := ControlList.GetInteractiveControl(FControlUp);
end;

procedure TAHMInteractiveControl.SetNavigateUp(Control: TAHMInteractiveControl);
begin
  if Assigned(Control) then FControlUp := Control.Name
                       else FControlUp := '';
end;

function TAHMInteractiveControl.GetNavigateDown: TAHMInteractiveControl;
begin
  with ApplicationManagerInstance.ThemeManager.InterfaceDescriptor do
    Result := ControlList.GetInteractiveControl(FControlDown);
end;

procedure TAHMInteractiveControl.SetNavigateDown(Control: TAHMInteractiveControl);
begin
  if Assigned(Control) then FControlDown := Control.Name
                       else FControlDown := '';
end;

function TAHMInteractiveControl.GetNavigateLeft: TAHMInteractiveControl;
begin
  with ApplicationManagerInstance.ThemeManager.InterfaceDescriptor do
    Result := ControlList.GetInteractiveControl(FControlLeft);
end;

procedure TAHMInteractiveControl.SetNavigateLeft(Control: TAHMInteractiveControl);
begin
  if Assigned(Control) then FControlLeft := Control.Name
                       else FControlLeft := '';
end;

function TAHMInteractiveControl.GetNavigateRight: TAHMInteractiveControl;
begin
  with ApplicationManagerInstance.ThemeManager.InterfaceDescriptor do
    Result := ControlList.GetInteractiveControl(FControlRight);
end;

procedure TAHMInteractiveControl.SetNavigateRight(Control: TAHMInteractiveControl);
begin
  if Assigned(Control) then FControlRight := Control.Name
                       else FControlRight := '';
end;

function TAHMInteractiveControl.GetState: TAHMControlState;
begin
  // If control is not visible, then state is hidden
  if not FVisible then
  begin
    // ...unless we're focused (i.e. something hid control whilst focused)
    if FFocused then Result := csDisabled
                else Result := csHidden;
  end
  // If control is not enabled, then state is disabled
  else if not FEnabled then
    Result := csDisabled
  // If control is not focused, then state is idle
  else if not FFocused then
    Result := csIdle
  // If control is not pressed, then state is focused
  else if not FPressed then
    Result := csFocused
  // Otherwise state is pressed
  else
    Result := csPressed;
end;

function TAHMInteractiveControl.GetHintText: String;
begin
  // Translate hint text property if one is set
  if FHint <> '' then
    Result := LanguageManagerInstance.Translate(FHint)
  else
    Result := '';
end;

procedure TAHMInteractiveControl.DidSomething;
begin
  // Reset idle timer
  FLastBusyTime := TimerInstance.CurrentTimeStamp;
  FControlIdled := False;
end;

procedure TAHMInteractiveControl.NormaliseMouseCoordinates(const X, Y: Single);
begin
  // First we account for our control position
  FMouseX := X - FXPos;
  FMouseY := Y - FYPos;

  // Now we account for viewport position if we have one
  if Assigned(FViewport) then
  begin
    FMouseX := FMouseX - FViewport.XOrigin;
    FMouseY := FMouseY - FViewport.YOrigin;
  end;
end;

procedure TAHMInteractiveControl.NormaliseMouseScale(var X, Y: Single);
begin
  // Scale current mouse coordinates to 0..1 range
  if (FRenderRect.Width > 0) and (FRenderRect.Height > 0) then
  begin
    X := FMouseX / FRenderRect.Width;
    Y := FMouseY / FRenderRect.Height;

    // Clamp coordinates in range
    if X < 0 then X := 0;
    if Y < 0 then Y := 0;
    if X > 1 then X := 1;
    if Y > 1 then Y := 1;
  end
  else
  begin
    // Set default position
    X := 0; Y := 0;
  end;
end;

procedure TAHMInteractiveControl.ClearProperties;
begin
  inherited;

  // Clear custom control properties
  FEnabled := True;
  FInitiallyEnabled := True;
  FMouseEnabled := True;
  FInitiallyMouseEnabled := True;
  FFocusedEffect := nil;
  FPressedEffect := nil;
  FCheckedEffect := nil;
  FControlUp := '';
  FControlDown := '';
  FControlLeft := '';
  FControlRight := '';
  FHint := '';
  FActions.ClearObjects;
  FActions.BaseList := nil;
end;

procedure TAHMInteractiveControl.RenderEffects;
begin
  // Delegate to method below using current control state
  RenderCustomEffects(GetOverlayRect, FFocused, FPressed, GetChecked);
end;

procedure TAHMInteractiveControl.RenderCustomEffects(Overlay: TAHMRectF; DrawFocus,
                                                     DrawPress, DrawCheck: Boolean);
var
  OldAlpha: Byte;
  FocusVal, PressVal: Single;
begin
  // Avoid compiler warnings
  FocusVal := 0.0;
  PressVal := 0.0;

  with TimerInstance do
  begin
    // Manage focused state transitions
    if FFocused then
    begin
      // Focused, so reset timer
      FFocusedStartTime := CurrentTimeStamp;
      FFocusedAnimating := True;
    end
    else if FFocusedAnimating then
    begin
      // Animating, check for animation period completed
      if HasDurationElapsed(FFocusedStartTime, FocusFadeDuration) then
        FFocusedAnimating := False
      else
        FocusVal := (CurrentTimeStamp - FFocusedStartTime) / FocusFadeDuration;
    end;

    // Manage pressed state transitions
    if FPressed then
    begin
      // Pressed, so reset timer
      FPressedStartTime := CurrentTimeStamp;
      FPressedAnimating := True;
    end
    else if FPressedAnimating then
    begin
      // Animating, check for animation period completed
      if HasDurationElapsed(FPressedStartTime, PressFadeDuration) then
        FPressedAnimating := False
      else
        PressVal := (CurrentTimeStamp - FPressedStartTime) / PressFadeDuration;
    end;
  end;

  // Render idle effect overlay first
  if Assigned(FEffect) and Assigned(FEffect.Bitmap) and not (Pressed or Focused) then
    FEffect.Bitmap.Render(Self, Overlay.X, Overlay.Y, Overlay.Width, Overlay.Height, True);

  // Render checked overlay
  if Assigned(FCheckedEffect) and Assigned(FCheckedEffect.Bitmap) and DrawCheck then
    FCheckedEffect.Bitmap.Render(Self, Overlay.X, Overlay.Y, Overlay.Width, Overlay.Height, True);

  // Render focused animation
  if Assigned(FFocusedEffect) and Assigned(FFocusedEffect.Bitmap) and FFocusedAnimating and DrawFocus then
  begin
    OldAlpha := FOwnGamma.InternalAlpha;
    FOwnGamma.InternalAlpha := Hi(OldAlpha * Round(255 * (1.0 - FocusVal)));
    FFocusedEffect.Bitmap.Render(Self, Overlay.X, Overlay.Y, Overlay.Width, Overlay.Height, True);
    FOwnGamma.InternalAlpha := OldAlpha;
  end;

  // Render pressed animation
  if Assigned(FPressedEffect) and Assigned(FPressedEffect.Bitmap) and FPressedAnimating and DrawPress then
  begin
    OldAlpha := FOwnGamma.InternalAlpha;
    FOwnGamma.InternalAlpha := Hi(OldAlpha * Round(255 * (1.0 - PressVal)));
    FPressedEffect.Bitmap.Render(Self, Overlay.X, Overlay.Y, Overlay.Width, Overlay.Height, True);
    FOwnGamma.InternalAlpha := OldAlpha;
  end;
end;

function TAHMInteractiveControl.PreviewMouseDown(X, Y: Single): Boolean;
begin
  Focused := BoundsPoint(X, Y);
  MouseDown(X, Y);
  Result := True;
end;

function TAHMInteractiveControl.PreviewMouseMove(X, Y: Single; Down: Boolean): Boolean;
begin
  Focused := BoundsPoint(X, Y);
  MouseMove(X, Y, Down);
  Result := True;
end;

function TAHMInteractiveControl.PreviewMouseUp(X, Y: Single): Boolean;
begin
  Focused := BoundsPoint(X, Y);
  MouseUp(X, Y);
  Result := True;
end;

function TAHMInteractiveControl.PreviewMouseWheel(Delta: Integer): Boolean;
begin
  MouseWheel(Delta);
  Result := True;
end;

function TAHMInteractiveControl.ModifyActions: Integer;
begin
  // Interactive control supports editing actions, hint and text
  Result := 3;
end;

function TAHMInteractiveControl.Modify(Action: Integer): Boolean;
begin
{$IFDEF DESIGNER}
  case Action of
    0: // Use native language modify method to edit text value
       with LanguageManagerInstance do
       begin
         Result := Languages[NativeLanguageName].ModifyValue(String(FText), CAPTION_TEXT);
         if Result then FDisplayText := '';
       end;
    1: // Edit actions with list dialog
       Result := TfrmObjectList.ShowList(FActions, nil, nil);
    2: // Use native language modify method to edit hint value
       with LanguageManagerInstance do
         Result := Languages[NativeLanguageName].ModifyValue(String(FHint), CAPTION_HINT);
    else Result := False;
  end;

  if Result then DoModified;
{$ELSE}
  Result := False;
{$ENDIF}
end;

function TAHMInteractiveControl.ModifyCaption(Action: Integer): String;
begin
{$IFDEF DESIGNER}
  case Action of
    0: Result := MODIFY_TEXT;
    1: Result := MODIFY_ACTIONS;
    2: Result := MODIFY_HINT;
    else Result := '';
  end;
{$ELSE}
  Result := '';
{$ENDIF}
end;

function TAHMInteractiveControl.PageScroll(Direction: TAHMDirection): Boolean;
begin
  Result := False;
  DidSomething;
end;

function TAHMInteractiveControl.Scroll(Direction: TAHMDirection): Boolean;
begin
  Result := False;
  DidSomething;
end;

function TAHMInteractiveControl.SmoothScroll(DX, DY: Single): Boolean;
begin
  Result := False;
  DidSomething;
end;

procedure TAHMInteractiveControl.InvalidateUIObject(UIObject: TAHMUIObject);
begin
  inherited;

  // Determine appropriate type of UIObject change here...
  if (UIObject is TAHMEffect) and ((FFocusedEffect = UIObject) or
     (FPressedEffect = UIObject) or (FCheckedEffect = UIObject)) then
  begin
    // Handle effect changes
    InvalidateEffect;
  end;
end;

procedure TAHMInteractiveControl.MoveControl(XPos, YPos: Single);
begin
  inherited MoveControl(XPos, YPos);

  // Calculate absolute midpoint of our object
  FCentreX := FViewport.XOrigin + FXPos + (FWidth / 2);
  FCentreY := FViewport.YOrigin + FYPos + (FHeight / 2);

  // Calculate comparison point for navigation
  FNavigationX := FViewport.XOrigin + FXPos;
  FNavigationY := FViewport.YOrigin + FYPos;
end;

procedure TAHMInteractiveControl.SizeControl(Width, Height: Single);
begin
  inherited SizeControl(Width, Height);

  // Calculate absolute midpoint of our control
  FCentreX := FViewport.XOrigin + FXPos + (FWidth / 2);
  FCentreY := FViewport.YOrigin + FYPos + (FHeight / 2);

  // Calculate comparison point for navigation
  FNavigationX := FViewport.XOrigin + FXPos;
  FNavigationY := FViewport.YOrigin + FYPos;
end;

procedure TAHMInteractiveControl.SendKey(KeyCode: String);
begin
  DidSomething;

  // Descendants may choose to respond to direct keypresses
end;

procedure TAHMInteractiveControl.SendKey(KeyCode: Word; Shift: TShiftState);
begin
  DidSomething;

  // Descendants may choose to respond to direct keypresses
end;

procedure TAHMInteractiveControl.Press;
begin
  // Set pressed state for control
  FPressed := True;
  DidSomething;
  InvalidateEffect;

  // Descendants may do more sophisticated stuff here
end;

procedure TAHMInteractiveControl.UnPress;
begin
  // Set unpressed state for control
  FPressed := False;
  DidSomething;
  InvalidateEffect;

  // Descendants may do more sophisticated stuff here
end;

procedure TAHMInteractiveControl.Activate;
begin
  DidSomething;

  // Descendants may do more sophisticated stuff here
end;

procedure TAHMInteractiveControl.Cancel;
begin
  DidSomething;

  // Descendants may do more sophisticated stuff here
end;

procedure TAHMInteractiveControl.Idle;
begin
  // Raise action when we first enter idle state
  if not FControlIdled then FActions.InvokeAction(AC_ONIDLE);

  // Set flag so we don't idle repeatedly
  FControlIdled := True;

  // Descendants may do more sophisticated stuff here
end;

procedure TAHMInteractiveControl.MouseDown(X, Y: Single);
begin
  // Normalise screen coordinates to control coordinates
  NormaliseMouseCoordinates(X, Y);

  // Keep a note of mouse coordinates when button is clicked
  FMouseDownX := FMouseX;
  FMouseDownY := FMouseY;

  // Set control appearance to pressed state
  Press;
end;

procedure TAHMInteractiveControl.MouseUp(X, Y: Single);
begin
  // Normalise screen coordinates to control coordinates
  NormaliseMouseCoordinates(X, Y);

  // Set control appearance to unpressed state
  UnPress;

  // Mouse down/up sequence constitutes a click
  MouseClicked;
end;

procedure TAHMInteractiveControl.MouseMove(X, Y: Single; Down: Boolean);
begin
  // Record current mouse coordinates
  FMouseDX := FMouseX;
  FMouseDY := FMouseY;

  // Normalise screen coordinates to control coordinates
  NormaliseMouseCoordinates(X, Y);

  // Now convert movement to a proper delta value
  FMouseDX := FMouseX - FMouseDX;
  FMouseDY := FMouseY - FMouseDY;

  // Notify control of mouse movement
  MouseMoved(Down);
end;

procedure TAHMInteractiveControl.MouseClicked;
begin
  // Process actions for this mouse control
  ApplicationManagerInstance.ActionManager.ProcessAction(AC_OKAY, Self);
end;

procedure TAHMInteractiveControl.MouseMoved(Down: Boolean);
begin
  if Down then DidSomething;

  // Descendants may do more sophisticated stuff here
end;

procedure TAHMInteractiveControl.MouseWheel(Delta: Integer);
begin
  DidSomething;

  // Descendants may do more sophisticated stuff here
end;

procedure TAHMInteractiveControl.InitialiseEffect;
begin
  // Delegate to method below using current control state
  InitialiseCustomEffects(FFocused, FPressed, GetChecked);
end;

procedure TAHMInteractiveControl.InitialiseCustomEffects(Focused, Pressed, Checked: Boolean);
begin
  // Apply scaling & gamma effects to this control
  if Pressed and (FPressedEffect <> nil) then
  begin
    FOwnXScale := FPressedEffect.XScale;
    FOwnYScale := FPressedEffect.YScale;
    if Assigned(FPressedEffect.Gamma) then
      FOwnGamma.Assign(FPressedEffect.Gamma.Colour)
    else
      FOwnGamma.Assign(TAHMCanvasColour.White);
  end
  else if Focused and (FFocusedEffect <> nil) then
  begin
    FOwnXScale := FFocusedEffect.XScale;
    FOwnYScale := FFocusedEffect.YScale;
    if Assigned(FFocusedEffect.Gamma) then
      FOwnGamma.Assign(FFocusedEffect.Gamma.Colour)
    else
      FOwnGamma.Assign(TAHMCanvasColour.White);
  end
  else if Checked and (FCheckedEffect <> nil) then
  begin
    FOwnXScale := FCheckedEffect.XScale;
    FOwnYScale := FCheckedEffect.YScale;
    if Assigned(FCheckedEffect.Gamma) then
      FOwnGamma.Assign(FCheckedEffect.Gamma.Colour)
    else
      FOwnGamma.Assign(TAHMCanvasColour.White);
  end
  else if FEffect <> nil then
  begin
    FOwnXScale := FEffect.XScale;
    FOwnYScale := FEffect.YScale;
    if Assigned(FEffect.Gamma) then
      FOwnGamma.Assign(FEffect.Gamma.Colour)
    else
      FOwnGamma.Assign(TAHMCanvasColour.White);
  end
  else
  begin
    FOwnXScale := 1.0;
    FOwnYScale := 1.0;
    FOwnGamma.Assign(TAHMCanvasColour.White);
  end;

  // Effect is now initialised
  FInitialisedEffect := True;
end;

procedure TAHMInteractiveControl.Rendering;
begin
  inherited;

  // Check for control entering idle state
  if TimerInstance.HasDurationElapsed(FLastBusyTime, FIdleTimeout) then Idle;
end;

procedure TAHMInteractiveControl.Assign(Source: TPersistent);
begin
  inherited Assign(Source);

  if Source is TAHMInteractiveControl then
    with Source as TAHMInteractiveControl do
    begin
      Self.Enabled := Enabled;
      Self.InitiallyEnabled := InitiallyEnabled;
      Self.MouseEnabled := MouseEnabled;
      Self.InitiallyMouseEnabled := InitiallyMouseEnabled;
      Self.FocusedEffect := FocusedEffect;
      Self.PressedEffect := PressedEffect;
      Self.CheckedEffect := CheckedEffect;
      Self.Hint := Hint;
      Self.NavigateUpName := NavigateUpName;
      Self.NavigateDownName := NavigateDownName;
      Self.NavigateLeftName := NavigateLeftName;
      Self.NavigateRightName := NavigateRightName;
      Self.Actions.ClearObjects;
      Self.Actions.BaseList := Actions;
    end;
end;

procedure TAHMInteractiveControl.AssignContents(Source: TAHMUIObject);
begin
  inherited;

  with Source as TAHMInteractiveControl do
    Self.Actions.AssignContents(Actions);
end;

procedure TAHMInteractiveControl.SaveAsXML(Lines: TStrings);
var
  oAncs: TAHMInteractiveControl;
  sWork: String;
begin
  // Determine which ancestor to compare against - real or empty
  oAncs := TAHMInteractiveControl(ObjectToCompare);

  // We need to explicitly save comments in overridden method
  SaveXMLComments(Lines);

  // Only populate attributes that differ from our ancestor
  Lines.Add(TABS[2] + TagBegin);

  // Do we need to store size? (only if no style is assigned)
  if (FStyle = nil) and
     (((FVirtWidth <> 0) and (FVirtWidth <> oAncs.VirtualWidth)) or
     ((FVirtHeight <> 0) and (FVirtHeight <> oAncs.VirtualHeight))) then
    Lines.Add(TABS[3] + TGB+TAG_SIZE+TGC+ IntToStr(FVirtWidth) + ',' +
              IntToStr(FVirtHeight) + TGE+TAG_SIZE+TGC);

  if (Text <> '') and (Text <> oAncs.Text) then
    Lines.Add(TABS[3] + TGB+TAG_TEXT+SPC+ATTR_NAME+ATB + EncodeXML(Text) + ATE);
  if (FHint <> '') and (FHint <> oAncs.Hint) then
    Lines.Add(TABS[3] + TGB+TAG_HINT+SPC+ATTR_NAME+ATB + EncodeXML(FHint) + ATE);

  // Do we need meta data element?
  if ((FMetaValue <> '') and (FMetaValue <> oAncs.MetaValue)) or
     ((FMetaGroup <> '') and (FMetaGroup <> oAncs.MetaGroup)) or
     ((FMetaIndex <> '') and (FMetaIndex <> oAncs.MetaIndex)) then
  begin
    sWork := TGB+TAG_META_VALUE;
    if (FMetaValue <> '') and (FMetaValue <> oAncs.MetaValue) then
      sWork := sWork + SPC+ATTR_NAME+ATB + EncodeXML(FMetaValue) + ATQ;
    if (FMetaGroup <> '') and (FMetaGroup <> oAncs.MetaGroup) then
      sWork := sWork + SPC+ATTR_GROUP+ATB + EncodeXML(FMetaGroup) + ATQ;
    if (FMetaIndex <> '') and (FMetaIndex <> oAncs.MetaIndex) then
      sWork := sWork + SPC+ATTR_INDEX+ATB + EncodeXML(FMetaIndex) + ATQ;
    Lines.Add(TABS[3] + sWork + ETE);
  end;

  // Do we need to store actions ?
  if FActions.IsDifferent(oAncs.Actions) then
  begin
    FActions.XMLIndent := 2;
    FActions.XMLTagName := TAG_ACTIONS;
    FActions.SaveDiffsAsXML(oAncs.Actions, Lines);
  end;

  // Do we need depth element?
  if (FDepth <> dtMidground) and (FDepth <> oAncs.Depth) then
    Lines.Add(TABS[3] + TGB+TAG_DEPTH+TGC + Depth2Str(FDepth) + TGE+TAG_DEPTH+TGC);

  // Do we need placement element?
  if (FPlacement <> nil) and (FPlacement <> oAncs.Placement) then
    Lines.Add(TABS[3] + TGB+TAG_PLACEMENT+SPC+ATTR_NAME+ATB + EncodeXML(FPlacement.Name) + ATE);

  // Do we need location element?
  with FLayoutLocn do
    if (Layout <> nil) and (not Equals(oAncs.Location)) then
    begin
      sWork := TGB+TAG_LAYOUT+SPC+ATTR_NAME+ATB + EncodeXML(Layout.Name) + ATQ;
      if Position <> 0 then
        sWork := sWork + SPC+ATTR_POSITION+ATB + IntToStr(Position) + ATQ;
      if Row <> 0 then
        sWork := sWork + SPC+ATTR_ROW+ATB + IntToStr(Row) + ATQ;
      if Column <> 0 then
        sWork := sWork + SPC+ATTR_COL+ATB + IntToStr(Column) + ATQ;
      Lines.Add(TABS[3] + sWork + ETE);
    end;

  // Do we need control navigation element?
  if ((FControlUp <> '') and (FControlUp <> oAncs.NavigateUpName)) or
     ((FControlDown <> '') and (FControlDown <> oAncs.NavigateDownName)) or
     ((FControlLeft <> '') and (FControlLeft <> oAncs.NavigateLeftName)) or
     ((FControlRight <> '') and (FControlRight <> oAncs.NavigateRightName)) then
  begin
    sWork := TGB+TAG_NAVIGATION;
    if (FControlUp <> '') and (FControlUp <> oAncs.NavigateUpName) then
      sWork := sWork + SPC+ATTR_UP+ATB + EncodeXML(FControlUp) + ATQ;
    if (FControlDown <> '') and (FControlDown <> oAncs.NavigateDownName) then
      sWork := sWork + SPC+ATTR_DOWN+ATB + EncodeXML(FControlDown) + ATQ;
    if (FControlLeft <> '') and (FControlLeft <> oAncs.NavigateLeftName) then
      sWork := sWork + SPC+ATTR_LEFT+ATB + EncodeXML(FControlLeft) + ATQ;
    if (FControlRight <> '') and (FControlRight <> oAncs.NavigateRightName) then
      sWork := sWork + SPC+ATTR_RIGHT+ATB + EncodeXML(FControlRight) + ATQ;
    Lines.Add(TABS[3] + sWork + ETE);
  end;

  // Do we need status element?
  if ((not InitiallyVisible) or (InitiallyVisible <> oAncs.InitiallyVisible)) or
     ((not FInitiallyEnabled) or (FInitiallyEnabled <> oAncs.InitiallyEnabled)) or
     ((not FInitiallyMouseEnabled) or (FInitiallyMouseEnabled <> oAncs.InitiallyMouseEnabled)) then
  begin
    sWork := TGB+TAG_STATUS;
    if (not InitiallyVisible) or (InitiallyVisible <> oAncs.InitiallyVisible) then
      sWork := sWork + SPC+ATTR_VISIBLE+ATB + Bool2Str(InitiallyVisible) + ATQ;
    if (not FInitiallyEnabled) or (FInitiallyEnabled <> oAncs.InitiallyEnabled) then
      sWork := sWork + SPC+ATTR_ENABLED+ATB + Bool2Str(FInitiallyEnabled) + ATQ;
    if (not FInitiallyMouseEnabled) or (FInitiallyMouseEnabled <> oAncs.InitiallyMouseEnabled) then
      sWork := sWork + SPC+ATTR_MOUSE+ATB + Bool2Str(FInitiallyMouseEnabled) + ATQ;
    Lines.Add(TABS[3] + sWork + ETE);
  end;

  // Do we need effect elements?
  if (FEffect <> nil) and (FEffect <> oAncs.Effect) then
    Lines.Add(TABS[3] + FEffect.XMLReference);
  if (FFocusedEffect <> nil) and (FFocusedEffect <> oAncs.FocusedEffect) then
    Lines.Add(TABS[3] + FFocusedEffect.XMLReference(TAG_FOCUSED));
  if (FPressedEffect <> nil) and (FPressedEffect <> oAncs.PressedEffect) then
    Lines.Add(TABS[3] + FPressedEffect.XMLReference(TAG_PRESSED));
  if (FCheckedEffect <> nil) and (FCheckedEffect <> oAncs.CheckedEffect) then
    Lines.Add(TABS[3] + FCheckedEffect.XMLReference(TAG_CHECKED));


  Lines.Add(TABS[2] + TagEnd);
end;

procedure TAHMInteractiveControl.BuildProperties(Properties: TAHMUIPropertyList);
begin
{$IFDEF DESIGNER}
  inherited;

  // Build custom interactive control properties
  Properties.Remove('Effect');
  Properties.Add('Checked Effect', 'CheckedEffect', CAT_APPEARANCE, HINT_CHECKEFFECT);
  Properties.Add('Focused Effect', 'FocusedEffect', CAT_APPEARANCE, HINT_FOCUSEFFECT);
  Properties.Add('Idle Effect', 'Effect', CAT_APPEARANCE, HINT_EFFECT);
  Properties.Add('Pressed Effect', 'PressedEffect', CAT_APPEARANCE, HINT_PRESSEFFECT);
  Properties.Add('Hint', 'Hint', CAT_CONTENT, HINT_HINT);
  Properties.Add('Actions', 'Actions', CAT_DESIGN, HINT_ACTIONS);
  Properties.Add('Enabled', 'InitiallyEnabled', CAT_DESIGN, HINT_ENABLED);
  Properties.Add('Mouse Enabled', 'InitiallyMouseEnabled', CAT_DESIGN, HINT_MOUSEENABLED);
  Properties.Add('Up Control', 'NavigateUpControl', CAT_NAVIGATION, HINT_UPCONTROL);
  Properties.Add('Down Control', 'NavigateDownControl', CAT_NAVIGATION, HINT_DOWNCONTROL);
  Properties.Add('Left Control', 'NavigateLeftControl', CAT_NAVIGATION, HINT_LEFTCONTROL);
  Properties.Add('Right Control', 'NavigateRightControl', CAT_NAVIGATION, HINT_RIGHTCONTROL);
{$ENDIF}
end;


end.
